package com.informa.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.DocumentResult;
import org.dom4j.io.DocumentSource;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;

public class XmlUtils {


	private static final String UTF_8 = "UTF-8";

	public static Document transform(String sourceText, Resource styleSheet) {
		return transform(sourceText, styleSheet, new Properties());
	}

	public static Document transform(String sourceText, Resource styleSheet, Properties params, Map<String,String> resources) {
		try {
			Source source = new DocumentSource(DocumentHelper.parseText(sourceText));
			Source xsltSource = new StreamSource(styleSheet.getInputStream());
			DocumentResult result = new DocumentResult();


			transform( params, resources, source, xsltSource, result );

			// return the transformed document
			return result.getDocument();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Document transform(String sourceText, Resource styleSheet, Properties params) {
		return transform(sourceText, styleSheet, params, new HashMap<String, String>());
	}

	private static void transform(final Properties params, final Map<String,String> resources, final Source source,
			final Source xsltSource, final Result result)
			throws TransformerConfigurationException, TransformerException {

		TransformerFactory factory = new net.sf.saxon.TransformerFactoryImpl();
		Transformer transformer = factory.newTransformer(xsltSource);


		transformer.setURIResolver(new URIResolver() {

			public Source resolve(String href, String base)
					throws TransformerException {

				try {
					return new DocumentSource(DocumentHelper.parseText(resources.get(href)));
				} catch (DocumentException e) {
					throw new RuntimeException(e);
				}

			}

		});


		for(Entry<Object, Object> param : params.entrySet()) {
			transformer.setParameter( param.getKey().toString(), param.getValue());
		}

		transformer.transform(source, result);
	}


	public static String toCanonicalForm(String xml) {
		return xml.replaceAll(">\\s+<", "><");
	}

	public static String transformToString(String sourceXml, Resource styleSheet, Properties params) {
		return transformToString(sourceXml, styleSheet, params, new HashMap<String, String>());
	}

	public static String transformToString(String sourceXml, Resource styleSheet, Properties params, Map<String,String> resources) {
		try {
			return transformToString(sourceXml, styleSheet.getInputStream(), params, resources);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public static String transformToString( String sourceXml, InputStream styleSheet, Properties params ) {
		return transformToString( sourceXml, styleSheet, params, new HashMap<String, String>() );
	}

	public static String transformToString(String sourceXml, InputStream styleSheet, Properties params, Map<String,String> resources) {

		StringWriter htmlOutput = new StringWriter();

		try {
			Source xsltSource = new StreamSource(styleSheet);
			Source source = new StreamSource(new StringReader(sourceXml) );
			Result result = new StreamResult(htmlOutput);

			transform( params, resources, source, xsltSource, result );

		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		return htmlOutput.toString();
	}


	public static String transformToString(String sourceText, Resource styleSheet)
    {
        return transformToString(sourceText, styleSheet, new Properties());
    }


	public static Document transform(Resource sourceResource, Resource styleSheet) {
		String text;
		try {
			text = FileCopyUtils.copyToString(new InputStreamReader(
					sourceResource.getInputStream()));
			return transform(text, styleSheet);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}


	public static List<String> valuesForEach(List<Node> nodes) {

		List<String> values = new ArrayList<String>();
		for(Node node : nodes) {
			values.add(node.getText());
		}

		return values;
	}


	public static String asPrettyXml(Document document) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		OutputFormat format = OutputFormat.createPrettyPrint();
		format.setEncoding(UTF_8);

		try {

			XMLWriter writer = new XMLWriter( out, format );
			writer.write( document );
			writer.close();
			String prettyXml = new String(out.toByteArray(), UTF_8);
			return prettyXml;

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static String asPrettyXml(Node node) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		OutputFormat format = OutputFormat.createPrettyPrint();
		format.setEncoding(UTF_8);

		try {

			XMLWriter writer = new XMLWriter( out, format );
			writer.write( node );
			writer.close();
			String prettyXml = new String(out.toByteArray(), UTF_8);
			return prettyXml;

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public static boolean exists(Node node, String xpath) {

		List<Node> nodes = node.selectNodes(xpath);
		boolean exists = nodes != null && !nodes.isEmpty();

		return exists;
	}

	public static List<String> valuesAt(String xpath, String xml) {
		return valuesAt( xpath, asDocument( xml ));
	}

	@SuppressWarnings("unchecked")
	public static List<String> valuesAt(String xpath, Document xml) {

		List<Node> nodes = xml.selectNodes(xpath);
		List<String> values = valuesForEach(nodes);

		return values;
	}

	public static boolean exists(String xpath, String xml) {
		return !valuesAt( xpath, xml ).isEmpty();
	}

	public static String valueAt(String xpath, Document xml) {

		Node node = xml.selectSingleNode(xpath);

		return node.getText();
	}

	public static String valueAt(String xpath, String xml) {
		return valueAt(xpath, asDocument(xml));
	}

	@SuppressWarnings("unchecked")
	public static String replaceText(String xml, String xpath, String text) {

		Document document = asDocument(xml);

		List<Node> nodes = document.selectNodes( xpath );

		for( Node node : nodes ) {
			node.setText(text);
		}

		return document.asXML();
	}
	
	public static String addAttribute(String xml, String elementXPath, String attributeName, String value) {
		Document document = asDocument(xml);
		
		Node node = document.selectSingleNode(elementXPath);
		Assert.notNull( node );
		Element element;

		try {
			element = (Element) node;
		} catch (ClassCastException cce) {
			throw new IllegalArgumentException("Node found by XPath was not an element");
		}
		
		element.addAttribute(attributeName, value);
		
		return document.asXML();
	}

	public static Document asDocument(String xml) {
		try {
			return DocumentHelper.parseText( xml );
		} catch (DocumentException e) {
			throw new RuntimeException(e);
		}
	}

}
